/**
 * Copyright (c) 2010 Sense Tecnic Systems Inc.
 */
package com.testcase.vincentApp;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Observable;

import android.content.SharedPreferences;
import android.content.res.Resources;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationManager;
import android.os.Handler;
import android.util.Log;

import com.sensetecnic.client.SenseTecnicClient;
import com.sensetecnic.client.SenseTecnicException;
import com.testcase.R;

/**
 * This class manages the scheduling for sensor updates and location provider
 * updates. It also holds the methods for gathering sensor data at each update
 * interval and posting it to Sense Tecnic.
 * 
 * TODO: vtsao - entire thing is just a timer...relies on phone not killing it,
 * daemon thread, etc...
 * 
 * TODO: vtsao - discuss hidden contracts (and passing all kinds of things
 * around to callbacks and stuff...maybe better static way of getting those
 * things?), maybe should be two things (?) TODO: vtsao - discuss hacky model a
 * bit, should use a content provider
 * 
 * @author Vincent Tsao
 */
class SensorManager extends Observable implements SensorEventListener
{
	// TODO: vtsao - quick and dirty way of helping the observer
	enum SensorUpdated
	{
		LOC, ACC, MAG, LIGHT
	}

	// all sense tecnic sensors created by this app will have the app name
	private static final String ANDROID_SENSORS = "androidsensors";

	// sense tecnic sensor name suffixes this app will use
	private static final String LOC_SENSOR_NAME = "location";
	private static final String ACC_SENSOR_NAME = "acc";

	private static final String STS_BASE_URL = "http://demo.sensetecnic.com/SenseTecnic/api";

	private static final float MIN_DISTANCE = 0;

	private static SensorManager singleton = null;

	private Resources resources;
	private SharedPreferences preferences;
	private LocationManager locationManager;
	private android.hardware.SensorManager androidSensorManager;

	private LocationUpdateHandler locationUpdateHandler;
	private PostSensorDataTask sensorUpdateHandler;
	private Handler handler;

	private SenseTecnicClient stsClient;

	// TODO: vtsao - quick and dirty model
	private double latestLat;
	private double latestLng;
	private double latestAcc;
	private double latestMag;
	private double latestLight;

	// TODO: vtsao - the hacks just keep getting better!
	private Date latestLocTs;
	private Date latestAccTs;
	private Date latestMagTs;
	private Date latestLightTs;

	// TODO: vtsao - hack to prevent more than one acc reading
	private boolean isAccToBeRead;

	/**
	 * 
	 * @return
	 */
	public static SensorManager getSensorManager ()
	{
		if (singleton == null)
		{
			singleton = new SensorManager ();
			return singleton;
		}

		return singleton;
	}

	private SensorManager ()
	{
		locationUpdateHandler = new LocationUpdateHandler ();
		handler = new Handler ();

		stsClient = new SenseTecnicClient ();
		stsClient.setStsBaseUrl (STS_BASE_URL);

		latestLocTs = null;
		latestAccTs = null;
		latestMagTs = null;
		latestLightTs = null;

		isAccToBeRead = false;
	}

	/**
	 * 
	 * 
	 * @param resources
	 * @param preferences
	 * @param locationManager
	 */
	void initSensorManager (Resources resources, SharedPreferences preferences, LocationManager locationManager, android.hardware.SensorManager androidSensorManager)
	{
		this.resources = resources;
		this.preferences = preferences;
		this.locationManager = locationManager;
		this.androidSensorManager = androidSensorManager;

		sensorUpdateHandler = new PostSensorDataTask (resources, preferences, this, handler);
	}

	/**
	 * Schedules location updates at the specified interval. Because location
	 * cannot be updated on demand (the Android model doesn't work like that),
	 * we request location updates at an interval less than the sensor update
	 * interval and get the last best known location during a location sensor
	 * update.
	 * 
	 * May be called repeatedly.
	 * 
	 * @param updateInterval
	 */
	void startLocationUpdates (int updateInterval)
	{
		// TODO: vtsao - this cannot just be "one minute before" or something as
		// it will accumulate overtime
		// updateInterval = updateInterval -

		String provider = locationManager.getBestProvider (new Criteria (), true);
		locationManager.requestLocationUpdates (provider, new Long (updateInterval), MIN_DISTANCE, locationUpdateHandler);
	}

	/**
	 * Starts sensor updates. May be called repeatedly, and will update the time
	 * interval at which sensors are updated by reading the user preference.
	 */
	void startSensorUpdates ()
	{
		stopSensorUpdates ();

		// get the new update interval
		String updateIntervalKey = resources.getString (R.string.updateIntervalKey);
		int updateInterval = Integer.parseInt (preferences.getString (updateIntervalKey, resources.getString (R.string.defaultUpdateInterval)));

		// create a new "timer" with that update interval
		handler.removeCallbacks (sensorUpdateHandler);
		handler.postDelayed (sensorUpdateHandler, updateInterval);

		// TODO: vtsao - only schedule location updates if it's on
		startLocationUpdates (updateInterval);

		Log.d (AndroidSensors.TAG, "Sensor updates started with update interval at " + updateInterval);
	}

	/**
	 * Stops all sensor updates.
	 */
	void stopSensorUpdates ()
	{
		handler.removeCallbacks (sensorUpdateHandler);

		// TODO: vtsao - only stop location updates if it's on
		locationManager.removeUpdates (locationUpdateHandler);

		Log.d (AndroidSensors.TAG, "Sensor updates have been stopped.");
	}

	/**
	 * 
	 */
	void updateLocation ()
	{
		Location location = locationUpdateHandler.getLastKnownLocation ();

		if (location == null)
		{
			Log.d (AndroidSensors.TAG, "Cannot read location, nothing will be sent.");
			return;
		}

		double lat = location.getLatitude ();
		double lng = location.getLongitude ();

		postToSenseTecnic (LOC_SENSOR_NAME, "", "0", lat, lng);

		Log.d (AndroidSensors.TAG, "Read location sensor (" + lat + ", " + lng + ") and sent to Sense Tecnic.");

		// keep the latest location cached and notify the view to update
		latestLat = lat;
		latestLng = lng;
		latestLocTs = new Date ();
		setChanged ();
		notifyObservers (SensorUpdated.LOC);
	}

	/**
	 * 
	 */
	void updateAccelerometer ()
	{
		isAccToBeRead = true;

		Sensor accSensor = androidSensorManager.getDefaultSensor (Sensor.TYPE_ACCELEROMETER);
		androidSensorManager.registerListener (this, accSensor, android.hardware.SensorManager.SENSOR_DELAY_FASTEST);
	}

	/**
	 * Helper function to post data to Sense Tecnic. Attempts to register the
	 * Android sensor first for that user if the sensor does not exist.
	 */
	private void postToSenseTecnic (String sensorName, String data, String value, double lat, double lng)
	{
		String username = preferences.getString (resources.getString (R.string.stsUsernameKey), null);
		String password = preferences.getString (resources.getString (R.string.stsPasswordKey), null);

		if (username == null || password == null)
		{
			// TODO: vtsao - perhaps report to user, but for now it will just
			// fail silently
			return;
		}
		
		// TODO: Jason
		/*
		 * This is where the HTTP 401 was sent from because I assume each phone is suppose
		 * to have their own sensor on Sense Tecnic but is not created by the app itself so
		 * it only currently only works for vincent.
		 * Commented line is from Vincent.
		 */
		//sensorName = username + "-" + ANDROID_SENSORS + "-" + sensorName;
		sensorName = "vtsao" + "-" + ANDROID_SENSORS + "-" + sensorName;

		// TODO: vtsao - register the sensor if it does not exist
		// naming scheme: {username}.androidsensors.{sensorName}
		// try
		// {
		// stsClient.getSensor (sensorName, STS_USERAME, STS_PASSWORD);
		// }
		// catch (SenseTecnicException e)
		// {
		// Log.d (AndroidSensors.TAG, e.getMessage ());
		// return;
		// }

		Map <String, String> values = new HashMap <String, String> ();
		values.put ("timestamp", String.valueOf (System.currentTimeMillis ()));
		values.put ("data", data);
		values.put ("value", value);
		values.put ("lat", String.valueOf (lat));
		values.put ("lng", String.valueOf (lng));

		try
		{
			stsClient.sendData (sensorName, username, password, values);
		}
		catch (SenseTecnicException e)
		{
			Log.d (AndroidSensors.TAG, e.getMessage ());
		}
	}

	/** @return */
	double getLatestLat ()
	{
		return latestLat;
	}

	/** @return */
	double getLatestLng ()
	{
		return latestLng;
	}

	/** @return */
	double getLatestAcc ()
	{
		return latestAcc;
	}

	/** @return */
	Date getLatestLocTs ()
	{
		return latestLocTs;
	}

	/** @return */
	Date getLatestAccTs ()
	{
		return latestAccTs;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.hardware.SensorEventListener#onSensorChanged(android.
	 * hardware.SensorEvent)
	 */
	public void onSensorChanged (SensorEvent event)
	{
		if (!isAccToBeRead)
			return;

		isAccToBeRead = false;

		// once we get a reading, we post to sense tecnic and unregister
		// the listener

		float accX = event.values[0];
		float accY = event.values[1];
		float accZ = event.values[1];

		postToSenseTecnic (ACC_SENSOR_NAME, "Accelerometer X-axis", String.valueOf (accX), 0, 0);
		// postToSenseTecnic (ACC_SENSOR_NAME, "Accelerometer Y-axis",
		// String.valueOf (accX), 0, 0);
		// postToSenseTecnic (ACC_SENSOR_NAME, "Accelerometer Z-axis",
		// String.valueOf (accX), 0, 0);
		Log.d (AndroidSensors.TAG, "Read accelerometer sensor (" + accX + ", " + accY + ", " + accZ + ") and sent to Sense Tecnic.");

		// keep the latest accelerometer cached and notify the view to
		// update
		latestAcc = accX;
		latestAccTs = new Date ();
		setChanged ();
		notifyObservers (SensorUpdated.ACC);

		androidSensorManager.unregisterListener (this, androidSensorManager.getDefaultSensor (Sensor.TYPE_ACCELEROMETER));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.hardware.SensorEventListener#onAccuracyChanged(android
	 * .hardware.Sensor, int)
	 */
	public void onAccuracyChanged (Sensor sensor, int accuracy)
	{
		// do nothing
	}
}
